package com.db.bytebuffer;

import java.nio.ByteBuffer;

public class IntegerMapImpl implements IntegerMap {
    private final ByteBuffer byteBuffer;
    private int byteBufferSize;

    private final int bucketsCount;
    private int freeItemIndex;

    private final ItemService itemService = new ItemService();
    private final MemoryService memoryService = new MemoryService();
    private final ByteBufferAdapter byteBufferAdapter = new ByteBufferAdapter();

    private static final float BUCKETS_RATIO = 0.5f;

    public IntegerMapImpl(ByteBuffer byteBuffer) {
        this.byteBuffer = byteBuffer;
        initByteBuffer();

        bucketsCount = (int) (byteBufferSize * BUCKETS_RATIO / ItemService.FIELDS_COUNT);
        freeItemIndex = (int) (byteBufferSize * BUCKETS_RATIO);
        memoryService.initFreeItems();
    }

    /**
     * Get element
     */
    @Override
    public Integer get(Integer key) {
        // Getting index
        int index = findBucketIndex(key);
        if (itemService.getKey(index) != Integer.MIN_VALUE && itemService.getKey(index) != key) {
            index = findCollisionIndex(key, itemService.getLink(index));
        }
        // Getting value
        if (index != Integer.MIN_VALUE && itemService.getKey(index) == key) {
            return itemService.getValue(index);
        } else {
            return null;
        }
    }

    /**
     * Put element
     */
    @Override
    public Integer put(Integer key, Integer value) {
        if (key == Integer.MIN_VALUE) {
            throw new IllegalArgumentException("Key can not be Integer.MIN_VALUE");
        }
        // Getting index
        int index = findBucketIndex(key);
        if (itemService.getKey(index) != Integer.MIN_VALUE && itemService.getKey(index) != key) {
            int collisionIndex = findCollisionIndex(key, itemService.getLink(index));
            if (collisionIndex != Integer.MIN_VALUE) {
                index = collisionIndex;
            } else {
                index = addCollisionItem(index);
            }
        }
        // Setting key and value
        int oldValue = itemService.getValue(index);
        itemService.setKey(index, key);
        itemService.setValue(index, value);
        // Returning old value
        if (oldValue != Integer.MIN_VALUE) {
            return oldValue;
        } else {
            return null;
        }
    }

    /**
     * Remove element
     */
    @Override
    public void remove(Integer key) {
        // Searching bucket
        int bucketIndex = findBucketIndex(key);
        if (itemService.getKey(bucketIndex) != Integer.MIN_VALUE) {
            // Removing bucket
            if (itemService.getKey(bucketIndex) == key) {
                // Bucket has a collision link
                int collisionIndex = itemService.getLink(bucketIndex);
                if (collisionIndex != Integer.MIN_VALUE) {
                    copyCollisionToBucket(collisionIndex, bucketIndex);
                    removeCollisionItem(collisionIndex, bucketIndex);
                }
                // Bucket has not collision link
                else {
                    itemService.cleanItem(bucketIndex);
                }
            }
            // Removing collision element
            else {
                int collisionIndex = findCollisionIndex(key, bucketIndex);
                int collisionReferenceIndex = findCollisionReferenceIndex(key, bucketIndex);
                if (collisionIndex != Integer.MIN_VALUE) {
                    removeCollisionItem(collisionIndex, collisionReferenceIndex);
                }
            }
        }
    }

    private void initByteBuffer() {
        byteBufferSize = byteBufferAdapter.limit();
        byteBufferSize = byteBufferSize - byteBufferSize % (ItemService.FIELDS_COUNT * 2);

        for (int i = 0; i < byteBufferSize; i++) {
            byteBufferAdapter.putInt(i, Integer.MIN_VALUE);
        }
    }

    private int findBucketIndex(int key) {
        return Math.abs(key % bucketsCount) * ItemService.FIELDS_COUNT;
    }

    private int findCollisionIndex(int key, int index) {
        while (index != Integer.MIN_VALUE) {
            if (itemService.getKey(index) == key) {
                return index;
            }
            index = itemService.getLink(index);
        }
        return Integer.MIN_VALUE;
    }

    private int findCollisionReferenceIndex(int key, int index) {
        while (itemService.getLink(index) != Integer.MIN_VALUE) {
            if (itemService.getKey(itemService.getLink(index)) == key) {
                return index;
            }
            index = itemService.getLink(index);
        }
        return Integer.MIN_VALUE;
    }

    private int addCollisionItem(int index) {
        while (itemService.getLink(index) != Integer.MIN_VALUE) {
            index = itemService.getLink(index);
        }
        int collisionItem = memoryService.allocateFreeItem();
        itemService.setLink(index, collisionItem);
        return collisionItem;
    }

    private void removeCollisionItem(int collisionItem, int referencedItem) {
        int nextCollisionItem = itemService.getLink(collisionItem);
        itemService.cleanItem(collisionItem);
        memoryService.deallocateFreeItem(collisionItem);
        itemService.setLink(referencedItem, nextCollisionItem);
    }

    private void copyCollisionToBucket(int collisionItem, int bucketItem) {
        itemService.setKey(bucketItem, itemService.getKey(collisionItem));
        itemService.setValue(bucketItem, itemService.getValue(collisionItem));

        itemService.cleanKey(collisionItem);
        itemService.cleanValue(collisionItem);
    }

    private class ItemService {
        private static final int FIELDS_COUNT = 3;

        private int getKey(int index) {
            return byteBufferAdapter.getInt(index);
        }

        private int getValue(int index) {
            return byteBufferAdapter.getInt(index + 1);
        }

        private int getLink(int index) {
            return byteBufferAdapter.getInt(index + 2);
        }

        private void setKey(int index, int key) {
            byteBufferAdapter.putInt(index, key);
        }

        private void setValue(int index, int value) {
            byteBufferAdapter.putInt(index + 1, value);
        }

        private void setLink(int index, int link) {
            byteBufferAdapter.putInt(index + 2, link);
        }

        private void cleanKey(int index) {
            byteBufferAdapter.putInt(index, Integer.MIN_VALUE);
        }

        private void cleanValue(int index) {
            byteBufferAdapter.putInt(index + 1, Integer.MIN_VALUE);
        }

        private void cleanLink(int index) {
            byteBufferAdapter.putInt(index + 2, Integer.MIN_VALUE);
        }

        private void cleanItem(int index) {
            cleanKey(index);
            cleanValue(index);
            cleanLink(index);
        }
    }

    private class MemoryService {
        private int allocateFreeItem() {
            int itemIndex = freeItemIndex;
            if (itemIndex != Integer.MIN_VALUE) {
                freeItemIndex = itemService.getLink(freeItemIndex);
                itemService.cleanLink(itemIndex);
                return itemIndex;
            } else {
                throw new RuntimeException("Can not allocate free item");
            }
        }

        private void deallocateFreeItem(int itemIndex) {
            itemService.setLink(itemIndex, freeItemIndex);
            freeItemIndex = itemIndex;
        }

        private void initFreeItems() {
            for (int i = freeItemIndex; i < byteBufferSize - ItemService.FIELDS_COUNT; i += ItemService.FIELDS_COUNT) {
                itemService.setLink(i, i + ItemService.FIELDS_COUNT);
            }
        }
    }

    private class ByteBufferAdapter {
        private static final int INTEGER_SIZE_BYTES = 4;

        private int getInt(int index) {
            return byteBuffer.getInt(index * INTEGER_SIZE_BYTES);
        }

        private void putInt(int index, int data) {
            byteBuffer.putInt(index * INTEGER_SIZE_BYTES, data);
        }

        private int limit() {
            return byteBuffer.limit() / INTEGER_SIZE_BYTES;
        }
    }
}
